package de.mmis.core.base.abstracttree.serializer.selector;

import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

import de.mmis.core.base.abstracttree.InnerNode;
import de.mmis.core.base.abstracttree.Tree.Type;
import de.mmis.core.base.abstracttree.serializer.DeserializableException;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper;
import de.mmis.core.base.gclass.GArray;
import de.mmis.core.base.gclass.GClass;
import de.mmis.core.base.gclass.GParameterized;

/**
 * {@link InstanceSelector} representing an abstract map type. The target type
 * determines the type of the map ({@link HashMap}s, {@link TreeMap}s,...)
 * 
 * @author Christian Koch
 */
public class MapSelector extends InstanceSelector {

	private final InnerNode struct;
	private final ClassLoader classLoader;

	public MapSelector(InnerNode struct, ClassLoader classLoader, String id,
			Map<String, Object> idmap) {
		super(id, idmap);

		this.struct = struct;
		this.classLoader = classLoader;
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public Object createInstance(GClass gclass)
			throws InstanceSelectorException {

		if (gclass instanceof GArray)
			throw new InstanceSelectorException("cannot convert " + this.struct
					+ " to type " + gclass + ": type is no map");

		Class<?> base = gclass.getBaseClass();

		if (gclass.getBaseClass() == Object.class)
			return createInstance(new GParameterized(Map.class, new GClass[] {
					GClass.fromClass(Object.class),
					GClass.fromClass(Object.class) }));

		if (!Map.class.isAssignableFrom(base))
			throw new InstanceSelectorException("cannot convert " + this.struct
					+ " to type " + gclass + ": type is no map");

		if (base.isInterface() || Modifier.isAbstract(base.getModifiers())) {
			if (base.isAssignableFrom(HashMap.class))
				return createInstance(new GParameterized(HashMap.class,
						GClass.getGenericsOf(gclass, Map.class)));

			if (base.isAssignableFrom(TreeMap.class))
				return createInstance(new GParameterized(TreeMap.class,
						GClass.getGenericsOf(gclass, Map.class)));

			throw new InstanceSelectorException(
					"map type \""
							+ gclass
							+ "\" is either abstract or an interface and there is no known method of instantiating a subclass of it");
		}

		GClass comptypeKey = GClass.fromClass(Object.class);
		GClass comptypeValue = GClass.fromClass(Object.class);
		GClass[] comptypes = GClass.getGenericsOf(gclass, Map.class);

		if (comptypes != null && comptypes.length >= 1)
			comptypeKey = comptypes[0];
		if (comptypes != null && comptypes.length >= 2)
			comptypeValue = comptypes[1];

		Map ret;
		try {
			ret = (Map) base.newInstance();
		} catch (InstantiationException e) {
			throw new InstanceSelectorException(
					"cannot instantiate Map of type \"" + gclass + "\"", e);
		} catch (IllegalAccessException e) {
			throw new InstanceSelectorException(
					"cannot instantiate Map of type \"" + gclass + "\"", e);
		}

		setID(ret);

		for (int i = 0; i < this.struct.getNumberOfSubTrees(); i++) {
			try {

				if (this.struct.getSubTree(i).getType() != Type.INNER
						|| ((InnerNode) this.struct.getSubTree(i))
								.getNumberOfSubTrees() != 2)
					throw new InstanceSelectorException(
							"Map Tree has wrong format: Sub-Tree " + i
									+ " is no 2-elem InnerNode");

				Object key = SerializerHelper.deserializeObject(
						((InnerNode) this.struct.getSubTree(i)).getSubTree(0),
						comptypeKey, this.classLoader, getIDMap());
				Object value = SerializerHelper.deserializeObject(
						((InnerNode) this.struct.getSubTree(i)).getSubTree(1),
						comptypeValue, this.classLoader, getIDMap());
				ret.put(key, value);
			} catch (DeserializableException e) {
				throw new InstanceSelectorException(
						"deserialization of collection element " + i
								+ " failed", e);
			}
		}
		return ret;
	}
}
